#!/usr/bin/env python
#
#   Copyright (C) 2006 Jonathan Saggau                                     
#   saggau@gmail.com                                                       
#                                                                          
#   This program is free software; you can redistribute it and/or modify  
#   it under the terms of the GNU General Public License as published by  
#   the Free Software Foundation; either version 2 of the License, or     
#   (at your option) any later version.                                    
#                                                                          
#   This program is distributed in the hope that it will be useful,        
#   but WITHOUT ANY WARRANTY; without even the implied warranty of        
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         
#   GNU General Public License for more details.                          
#                                                                          
#   You should have received a copy of the GNU General Public License     
#   along with this program; if not, write to the                         
#   Free Software Foundation, Inc.,                                        
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.  
#

import globals
import getfiles
import buffer

from model import Model #trigger model
import alerts # popups and sheets for alert msgs

import objc
from Foundation import *
from AppKit import *
from PyObjCTools import NibClassBuilder

from scosc import controller
from supyrconductor import pool, process, server
from trigger import deftxt

import os
DEFAULT_PATH = os.path.expanduser('~')

class TriggerAppDelegate(NibClassBuilder.AutoBaseClass):
    """App Delegate controls the main window"""
    _serverIP = '127.0.0.1' #may get this from shared defaults later
    _serverPort = 57110
    _clientIP = '127.0.0.1'
    _clientPort = 7000
    _sendToClient = False
    _searchInSubdirectories = False
    _soundFileSearchPath = DEFAULT_PATH
    
    #NSArrays instead of [] to avoid some fun pyobjc proxy memory problems
    _triggerModels = NSArray.alloc().init()
    _soundFiles = NSArray.alloc().init()
    _buffers = NSArray.alloc().init()
    _synthTypes = NSArray.alloc().init()
    _envTypes = NSArray.alloc().init()
    
    def init(self):
        """pyobjc's version of __init__(self)"""
        self = super(TriggerAppDelegate, self).init()
        if not self:
            return None
        self.setMaxVolume_(25)
        # Some of these are being watched from the GUI and so need to be KVC,
        # hence the getters and setters.  ehh.
        self.setSearchSubdirectories_(False)        
        self._makePools()
        self._app = NSApp()
        self.controller = controller.Controller(\
                                          (self._serverIP, self._serverPort),
                                          verbose = True, spew = True, 
                                          maxQLength = 3)
        self.controller.listSendMsg(server.notify(1))
        self._emptyBuffer = buffer.Buffer.alloc().\
                                       initWithController_BufferID_FilePath_(\
                                                  self.controller, None, None)
        #self.scInstance = process.start_local(verbose = True) 
        #fire up the server
        
        NSNotificationCenter.defaultCenter().\
                             addObserver_selector_name_object_(self,
                                                  "applicationWillTerminate:",
                                       NSApplicationWillTerminateNotification,
                                                                         None)
        return self

    def _makePools(self):
        self._triggerInputPool = pool.IntPool(0)
        self._nodePool = pool.IntPool(1000)
        self._bufferPool = pool.IntPool(1)

    def applicationWillTerminate_(self, notification):
        NSLog("Trigger is shutting down")
        self.controller.listSendMsg(server.quit())
        try:
            self.controller.receive("/done")
        except IOError, e:
            pass
            # there is no server to send the reply in some cases, but we want 
            # to wait for the timeout before giving up.
        #self.scInstance.kill()
        NSNotificationCenter.defaultCenter().removeObserver_name_object_(self,
                                                                   None, None)

    def setup_(self, sender):
        self._app.\
        beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
                                                        self.preferencesSheet,
                                                        self.mainWindow,
                                                        self, None, 0)

    def closeSetup_(self, sender):
        self.preferencesSheet.close()
        NSApplication.sharedApplication().endSheet_(self.preferencesSheet)

    def addTriggerModel_(self, sender):
        newNodeID = self._nodePool.get()
        newTriggerModel = Model.alloc().\
            initWithController_NodeID_Buffer_AppDelegate(\
                          self.controller, newNodeID, self._emptyBuffer, self)
        newTriggerModel.setInput_(self._triggerInputPool.get())
        self.trigArrayController.addObject_(newTriggerModel)

    def rmvTriggerModel_(self, sender):
        """
            1. Test the other triggers to see if the buffer is still needed
            2. run rmvBuffer (protects buffers other triggers need)
            3. Recycle the node ID and triggerID
            4. Remove the trigger from the triggerarray
        """
        if len(self.trigArrayController.arrangedObjects()) > 0:
            index = self.trigArrayController.selectionIndex()
            triggers = self.trigArrayController.selectedObjects()
            trigger = triggers[0] #only one selection possible
            self._triggerInputPool.recycle(trigger.input())
            self._nodePool.recycle(trigger.nodeID())
            self.trigArrayController.removeObjectAtArrangedObjectIndex_(index)
            self.rmvBuffer(trigger.bufferID())
                        
    def rmvBuffer(self, id):
        """remove buffer from the array controller iff no triggers need it"""
        triggers = self.trigArrayController.arrangedObjects()
        buffers = self.bufferArrayController.arrangedObjects()
        if len(triggers) > 0 and len(buffers) > 0:
            neededBufferIDs = [trigger.buffer().ID() for trigger in triggers]
            if id not in neededBufferIDs:
                unneeded = [trigger.buffer() for trigger in triggers \
                            if trigger.buffer().ID() == id]
                for buffer in unneeded:
                    if buffer is not self._emptyBuffer:
                        #protect the empty buffer from removal
                        buffer.close() # removes buffer from server
                        self._bufferPool.recycle(buffer.ID())
                        NSLog("Removing buffer : %s" %(buffer,))
                        self.bufferArrayController.removeObject_(buffer)
                        
    def setSoundFile_ForTrigger_(self, path, trig):
        """
            Poll current buffers for the sound file
            if the needed sound file is there:
                set the trigger to use it
            else:
                make a new buffer, put it in the buffer array and
                set the trigger to use it
        """
        oldBuff = trig.buffer()
        if oldBuff.filePath() is not path:
            buffers = self.bufferArrayController.arrangedObjects()
            try:
                useThisOne = [buff for buff in buffers\
                              if buff.filePath() is path][0]
                NSLog("Using current buffer ID: %s for path: %s" \
                      %(useThisOne.ID(), path))
            except IndexError:
                # this means that the file isn't in the buffers list
                ID = self._bufferPool.get()
                NSLog("Creating new buffer ID %s for path: %s" %(ID, path))
                useThisOne = buffer.Buffer.alloc().\
                                       initWithController_BufferID_FilePath_(\
                                                    self.controller, ID, path)
                self.bufferArrayController.addObject_(useThisOne)
            trig.setBuffer_(useThisOne)
            self.rmvBuffer(oldBuff)
            self._refreshWindows()

    def _refreshWindows(self):
        NSLog("+++REFRESHING WINDOWS+++")
        self._app.setWindowsNeedUpdate_(True)
#        for win in self._app.windows():
#            self.table.display()
#            #self.table.setNeedsDisplay()
#            #self.table.setNeedsDisplay_(True)
#            win.display()

    def resetButton_(self, sender):
        self.reset()
            
    def reset(self):
        """Clears the triggers list and starts over from zero without having
           to restart the application.  Settings for sound files dir not affected."""
        triggers = self.trigArrayController.arrangedObjects()
        for trig in triggers:
            trig.close() #kill all nodes
        self.trigArrayController.removeObjects_(triggers)
        buffers = self.bufferArrayController.arrangedObjects()
        self.bufferArrayController.removeObjects_(buffers)
        for buffer in buffers:
            buffer.close()
        self._makePools()

    def _clearSoundFiles(self):
        if len(self.soundFilesArrayController.arrangedObjects()) > 0:
            content = self.soundFilesArrayController.arrangedObjects()
            self.soundFilesArrayController.removeObjects_(content)
        assert(len(self.soundFilesArrayController.arrangedObjects()) == 0)
        
    def _search(self, openPanel): #move this to another file
        """Mostly handles the GUI stuff for doing
           a path search for sound files"""
        try:
            self.pathSearchProgressIndicator.startAnimation_(self)
            if openPanel:
                newpath, files = getfiles.openPanelFindAudioFiles(\
                                     recursive = self.searchSubdirectories())
                self.setSoundFileSearchPath_(newpath)
            else:
                files = getfiles.findAudioFiles(self.soundFileSearchPath(),
                                     recursive = self.searchSubdirectories())
            self._clearSoundFiles()
            self.soundFilesArrayController.addObjects_(files)
            self.pathSearchProgressIndicator.stopAnimation_(self)
            #TODO: reset the spinner to 12 o'clock here
            if files == []:
                NSLog("Trigger has no sound files in path %s" %path)
                alerts.alertMsgNoSheet("Alert", "No Sound Files in Path",\
                                       "OK")
            self.soundFileTableColumn.dataCell().setLineBreakMode_(\
                                                 NSLineBreakByTruncatingHead)
            #shows tail end of sound file path only in the view!
        except AttributeError:
            pass
            # On startup _search is called before all NIB connections are made
            # The initialization of the "Search Subdirectories" checkbox 
            # triggers this.
            # Basically, we don't want to do anything in this case.
        
    def sfOpenSearch_(self, sender):
        """Open a dialog box to search for sound files"""
        self._search(True)
        
    def sfSearch_(self, sender):
        """Run the search.  No Dialog"""
        self._search(False)
        
#getters n' setters for KVO sanity.  Ain't PyObjC fun?        
    def setMaxVolume_(self, volInt):
        self._maxVolume = volInt
        
    def maxVolume(self):
        return self._maxVolume
        
    def setSearchSubdirectories_(self, theBool):
        self._searchInSubdirectories = theBool
        self._search(False)
        
    def searchSubdirectories(self):
        return self._searchInSubdirectories
        
    def setSoundFileSearchPath_(self, pathStr):
        self._soundFileSearchPath = pathStr
        
    def soundFileSearchPath(self):
        return self._soundFileSearchPath
        
    def setSoundFiles_(self, theNSArray):
        self._soundFiles = theNSArray
        
    def buffers(self):
        return self._buffers
        
    def setBuffers_(self, theNSArray):
        self._buffers = theNSArray
        
    def soundFiles(self):
        return self._soundFiles
        
    def setTriggerModels_(self, theNSArray):
        self._triggerModels = theNSArray
        
    def triggerModels(self):
        return self._triggerModels
        
    def sendToClient(self):
        return self._sendToClient
        
    def setSendToClient_(self, boolVal):
        triggers = self.trigArrayController.arrangedObjects()
        for trigger in triggers:
            trigger.setSendToClient_(boolVal)
        self._sendToClient = boolVal
        
    def clientIP(self):
        return self._clientIP
        
    def setClientIP_(self, IP):
        triggers = self.trigArrayController.arrangedObjects()
        for trigger in triggers:
            trigger.setClientIP_(IP)
                    
    def serverIP(self):
        return self._serverIP
        
    def setServerIP_(self, IP):
        self._serverIP = IP
        self.reset()
        # start over when addressing a new server
        
    def clientPort(self):
        return self._clientPort
        
    def setClientPort_(self, port):
        triggers = self.trigArrayController.arrangedObjects()
        for trigger in triggers:
            trigger.setClientPort_(port)
        self._clientPort = port

    def serverPort(self):
        return self._serverPort
        
    def setServerPort_(self, port):
        self._serverPort = port
        self.reset()
        # start over when addressing a new server